﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;

namespace Zentrum.DI
{
	public partial class DefaultContainer : IContainer
	{
		private readonly Dictionary<Type,Func<CreationContext,object>> m_Items=new Dictionary<Type,Func<CreationContext,object>>();

		/// <summary>
		/// Attempts to resolve a type
		/// </summary>
		/// <typeparam name="T">The type to resolve</typeparam>
		/// <returns>An instance that is type compatible with T</returns>
		public T Resolve<T>()
		{
			var context=new CreationContext();
			var instance=Resolve(context,typeof(T));
			return (T)instance;
		}

		/// <summary>
		/// Registers an existing instance
		/// </summary>
		/// <typeparam name="T">The type of the instance</typeparam>
		/// <param name="item">The instance</param>
		public void RegisterInstance<T>(T item) where T:class
		{
			if(item==null) throw new ArgumentNullException("item");

			Type type=typeof(T);
			if(m_Items.ContainsKey(type)) throw new ArgumentException("type already registered: "+type.ToString());

			Func<CreationContext,object> lookup=context=>item;
			m_Items.Add(type,lookup);
		}

		public void Register<B,T>() where T:class
		{
			Register<B,T>(Lifetime.Transient);
		}

		public void Register<B,T>(Lifetime lifetime) where T:class
		{
			Type baseType=typeof(B);
			if(m_Items.ContainsKey(baseType)) throw new ArgumentException("type already registered: "+baseType.ToString());

			EnsureTypeConstructable(typeof(T));

			var typeInfo=typeof(T);
			Func<CreationContext,object> lookup=null;
			
			if(lifetime==Lifetime.Transient)
			{
				lookup=context=>
				{
					using(context.Scope(typeInfo))
					{
						return CreateType(context,typeInfo);
					}
				};
			}
			else
			{
				object instance=null;
				lookup=context=>
				{
					if(instance==null) 
					{
						using(context.Scope(typeInfo))
						{
							instance=CreateType(context,typeInfo);
						}
					}

					return instance;
				};
			}

			m_Items.Add(baseType,lookup);
		}

		private void EnsureTypeConstructable(Type type)
		{
			if(type.IsClass==false) throw new ContainerException("T must be a class");
			if(type.IsAbstract) throw new ContainerException("T is abstract");
		}

		private object CreateType(CreationContext context, Type typeInfo)
		{
			using(context.Scope(typeInfo))
			{
				var constructor=SelectConstructor(typeInfo);			
				var parameterInfo=constructor.GetParameters();	

				object[] parameters=new object[parameterInfo.Length];

				for(int i=0; i<parameterInfo.Length; i++)
				{
					Type type=parameterInfo[i].ParameterType;

					Func<CreationContext,object> lookup;
					if(m_Items.TryGetValue(type,out lookup)==false) throw new ContainerException("Type not registered: "+type.ToString());

					parameters[i]=lookup(context);
				}

				object instance=constructor.Invoke(parameters);
				return instance;
			}
		}

		private object Resolve(CreationContext context, Type type)
		{
			using(context.Scope(type))
			{
				Func<CreationContext,object> lookup;
				if(m_Items.TryGetValue(type,out lookup))
				{
					return lookup(context);
				}

				// The type isn't registered, so assume we want to create a transient instance
				EnsureTypeConstructable(type);

				object instance=CreateType(context,type);
				return instance;
			}
		}

		private ConstructorInfo SelectConstructor(Type typeInfo)
		{
			ConstructorInfo candidate=null;
			bool ambigious=false;

			foreach(var ctor in typeInfo.GetConstructors())
			{
				if(ctor.IsPublic==false) continue;

				if(candidate==null)
				{
					candidate=ctor;
				}
				else
				{
					if(ctor.GetParameters().Length>candidate.GetParameters().Length)
					{
						candidate=ctor;
						ambigious=false;
					}
					else if(ctor.GetParameters().Length==candidate.GetParameters().Length)
					{
						ambigious=true;
					}
				}
			}

			if(ambigious) throw new ArgumentException("Two or more constructors have the same arity: "+typeInfo.ToString());
			if(candidate==null) throw new ArgumentException("Could not find a public constructor: "+typeInfo.ToString());

			return candidate;
		}

	}
}
